﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
using System.Threading.Tasks;
using System.Xml;
using SIEGate.Core;
using SIEGate.Core.Measurements;
using SIEGate.Core.Measurements.Values;
using SIEGate.Core.Stats;

namespace SIEGateCoreTest.DataProcessorTests
{
    // This is a trivial implementation of network communication, done as an example of how to implement a NetworkCommunicator.
    // It should never be used live as it provides zero security and has high data overhead.
    // Additionally, TcpListener/TcpClient are superior to Sockets; Sockets are simpler but provide minimal infrastructure
    // Still a work in progress
    public class NaiveSocketNetworkCommunicator : NetworkCommunicator
    {
        private class TrackableStream
        {
            private readonly IFormatter m_formatter;
            private readonly Cluster m_statsCluster;
            private readonly NetworkStream m_networkStream;
            private readonly MemoryStream m_memoryStream;

            public TrackableStream(Cluster statsCluster, IFormatter formatter, NetworkStream networkStream, MemoryStream memoryStream)
            {
                m_formatter = formatter;
                m_statsCluster = statsCluster;
                m_networkStream = networkStream;
                m_memoryStream = memoryStream;
            }

            public bool CanRead
            {
                get { return m_networkStream.CanRead; }
            }

            public void Send(IMeasurement[] data)
            {
                // Over the long run, using IMeasurement here is going to be a problem since the below code assumes that data is Serializable.
                // That cannot be enforced in interfaces, only on classes, which we want to avoid due to the lack of multiple inheritence.
                m_formatter.Serialize(m_memoryStream, data);
                byte[] toSend = m_memoryStream.GetBuffer();
                // Can't use UpdateBytesSent here since nested classes don't have access to protected functions.
                // Don't want to make it public since that would allow other clases to change bytes sent inaccurately
                m_statsCluster.AddToCounter(NetworkCommunicator.StrStatBytesOut, toSend.Length);

                m_formatter.Serialize(m_networkStream, data);
            }

            public IMeasurement[] Receive()
            {
                IMeasurement[] data = (IMeasurement[])m_formatter.Deserialize(m_networkStream);

                // I don't know of another way to get the number of bytes that were received, and this assumes non-lossy data movement, which
                // is inaccurate.  The main point here is to clarify that STR_STAT_BYTES_IN (and STR_STAT_BYTES_OUT) must be updated when
                // there's network traffic.  In this case I'm relying on NetworkStream with serialization which isn't very good at tracking
                // how much data is passingthrough it, so the MemoryStream is necessary just to have some way to measure that number
                m_formatter.Serialize(m_memoryStream, data);
                byte[] toSend = m_memoryStream.GetBuffer();
                // Can't use UpdateBytesReceived here since nested classes don't have access to protected functions.
                // Don't want to make it public since that would allow other clases to change bytes received inaccurately
                // Note that this particular implementation has a bug where if the SIEGate is both the sender and received, twice as much
                // data is shown as received as actually i
                m_statsCluster.AddToCounter(NetworkCommunicator.StrStatBytesIn, toSend.Length);

                return data;
            }
        }

        private bool m_started = false;
        private TcpListener m_listener;
        private readonly Dictionary<TcpClient, TrackableStream> m_streams = new Dictionary<TcpClient, TrackableStream>();
        private readonly IFormatter m_formatter = new SoapFormatter();
        private readonly IList<Task> m_tasks = new List<Task>();
        private IPEndPoint m_endPoint;

        public NaiveSocketNetworkCommunicator(IPEndPoint endPoint, IMessageBusProxy busProxy, NetworkCommunicatorInfoBase info)
            : base(busProxy, info)
        {
            m_endPoint = endPoint;
        }

        private void CreateDeserializationThread(TrackableStream stream)
        {
            Task t = new Task(() =>
                {
                    while (m_started)
                    {
                        if (stream.CanRead)
                        {
                            try
                            {
                                // TODO: Log serialization errors. Not critical for NaiveTcpNetworkCommunicator
                                // since it should never be used live.
                                IMeasurement[] data = stream.Receive();
                                foreach (Guid guid in Info.OutgoingStreamIDs)
                                {
                                    MessageBusProxy.Send(new RawData(guid, new MeasurementListValue(data)));
                                }
                            }
                            catch (IOException e)
                            {
                                if (m_started && stream.CanRead)
                                {
                                    throw e;
                                }
                                // Otherwise we've already disconnected and the error doesn't matter
                            }
                            catch (XmlException e)
                            {
                                if (m_started && stream.CanRead)
                                {
                                    throw e;
                                }
                                // Otherwise we've already disconnected and the error doesn't matter
                            }
                        }
                    }
                }
            );
            t.Start();
            m_tasks.Add(t);
        }

        public void CloseClient(TcpClient client)
        {
            if (!m_started)
                return;
            m_streams.Remove(client);
            client.Close();
        }

        public void Send(IMeasurement[] data)
        {
            if (!m_started)
                return;
            foreach (TrackableStream stream in m_streams.Values)
            {
                stream.Send(data);
            }
        }

        public override void Start()
        {
            m_started = true;
            MessageBusProxy.Publish(Info.OutgoingStreamIDs);
            MessageBusProxy.Subscribe(Info.IncomingStreamIDs);
            if (IsListener)
            {
                m_listener = new TcpListener(m_endPoint);
                m_listener.Start();
                m_listener.BeginAcceptTcpClient(OnConnection, m_listener);
            }
            Task t = new Task(() =>
                {
                    while (m_started)
                    {
                        IMeasurement[] measurements = MessageBusProxy.Recv(256);
                        // No reason to send empty arrays.
                        if (measurements.Length > 0)
                        {
                            Send(measurements);
                        }
                    }
                }
            );
            t.Start();
            m_tasks.Add(t);
        }

        // This should not exist in other NetworkCommunicators.  It's necessary here because the same machine is both sender and receiver.
        // See TestNaiveTcpNetworkCommuniactor for more information.
        public bool IsListener { get; set; }

        private void OnConnection(IAsyncResult ar)
        {
            TcpListener listener = (TcpListener) ar.AsyncState;
            AddClient(listener.EndAcceptTcpClient(ar));
        }

        public override void Stop()
        {
            m_started = false;
            MessageBusProxy.UnPublish(Info.OutgoingStreamIDs);
            MessageBusProxy.UnSubscribe(Info.IncomingStreamIDs);
            foreach (TcpClient client in m_streams.Keys)
            {
                client.Close();
            }
            m_streams.Clear();
            m_tasks.Clear();
        }

        /// <summary>
        /// Connect to a remote NaiveSocketNetworkCommunicator on ipAddress on port
        /// </summary>
        /// <param name="ipAddress">The IP Address of the remote NaiveSocketNetworkCommunicator.</param>
        /// <param name="port">The port to connect to.</param>
        public TcpClient Connect(IPAddress ipAddress, int port)
        {
            TcpClient client = null;
            if (m_started)
            {
                client = new TcpClient();
                client.Connect(ipAddress, port);
                AddClient(client);
            }
            return client;
        }

        private void AddClient(TcpClient client)
        {
            if (m_streams.ContainsKey(client) || (client == null))
                return;
            TrackableStream stream = new TrackableStream(m_statsCluster,
                                                         m_formatter,
                                                         client.GetStream(),
                                                         new MemoryStream());
            Debug.Assert(m_streams != null);
            Debug.Assert(client != null);
            Debug.Assert(stream != null);
            m_streams.Add(client, stream);
            CreateDeserializationThread(stream);
        }
    }
}
